package jxtend.processor.eclipse.utils;

import java.lang.reflect.Array;

import jxtend.processor.eclipse.visitor.handler.impl.BindingTagBits;
import jxtend.processor.reflect.ReflectField;
import jxtend.processor.reflect.ReflectMethod;
import jxtend.processor.reflect.ReflectMethod.ResultMethod3Args;
import jxtend.processor.reflect.ReflectMethod.VoidMethod0Args;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.env.AccessRestriction;
import org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.PackageBinding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.util.HashtableOfPackage;
import org.eclipse.jdt.internal.compiler.util.HashtableOfType;

@SuppressWarnings("restriction")
public final class EclipseProcessorUtils {

	private static final ReflectField<SourceTypeBinding, FieldBinding[]> fieldsOfSourceTypeBinding =
		ReflectField.getField(SourceTypeBinding.class, FieldBinding[].class, "fields");
	private static final ReflectField<SourceTypeBinding, MethodBinding[]> methodsOfSourceTypeBinding =
		ReflectField.getField(SourceTypeBinding.class, MethodBinding[].class, "methods");
	private static final ReflectField<PackageBinding, HashtableOfType> knownTypesOfPackageBinding =
		ReflectField.getField(PackageBinding.class, HashtableOfType.class, "knownTypes");
	private static final ReflectField<PackageBinding, HashtableOfPackage> knownPackagesOfPackageBinding =
			ReflectField.getField(PackageBinding.class, HashtableOfPackage.class, "knownPackages");

	private static final VoidMethod0Args<HashtableOfType> rehashMethodOfHashtableOfType =
		ReflectMethod.voidMethod(HashtableOfType.class, "rehash");
	private static final VoidMethod0Args<HashtableOfPackage> rehashMethodOfHashtableOfPackage =
			ReflectMethod.voidMethod(HashtableOfPackage.class, "rehash");
	private static final ResultMethod3Args<ClassScope, SourceTypeBinding, SourceTypeBinding, PackageBinding, AccessRestriction>
		buildTypeMethodOfClassScope = ReflectMethod.method(ClassScope.class, SourceTypeBinding.class,
				"buildType", SourceTypeBinding.class, PackageBinding.class, AccessRestriction.class); 

	public ReferenceBinding superclass;
	public ReferenceBinding[] superInterfaces;
	private FieldBinding[] fields;
	private MethodBinding[] methods;
	
	public static void updateCompilerEnvironment(
			LookupEnvironment lookupEnvironment, CompilationUnitDeclaration[] compilationUnits) {
		for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
			deregisterTypeDeclarations(compilationUnit, true);
		}

		for (CompilationUnitDeclaration compilationUnit : compilationUnits) {
			lookupEnvironment.buildTypeBindings(compilationUnit, null);			
		}

		int unitCount = compilationUnits.length;
		boolean[] buildFieldsAndMethods = new boolean[unitCount];
		for (int i = 0; i < unitCount; i++) {
			buildFieldsAndMethods[i] = true;
		}

		lookupEnvironment.completeTypeBindings(compilationUnits, buildFieldsAndMethods, unitCount);
	}

	public static void updateTypeDeclaration(
			CompilationUnitDeclaration compilationUnit, TypeDeclaration typeDeclaration) {
		SourceTypeBinding enclosingTypeBinding =
			(typeDeclaration.enclosingType != null) ? typeDeclaration.enclosingType.binding : null; 
		ClassScope classScope = typeDeclaration.binding.scope;
		PackageBinding packageBinding = typeDeclaration.binding.fPackage;

//		TypeDeclaration[] typesInCompilationUnit = compilationUnit.types;
//		
//		if (typesInCompilationUnit != null) {
//			for (TypeDeclaration currentTypeDeclaration : typesInCompilationUnit) {
//				deregisterTypeDeclaration(compilationUnit, "", currentTypeDeclaration, true);
//			}
//		}

		TypeDeclaration[] typesInCompilationUnit = compilationUnit.types;
		
		if (typesInCompilationUnit != null) {
			for (TypeDeclaration currentTypeDeclaration : typesInCompilationUnit) {
				if ((currentTypeDeclaration.binding.tagBits & BindingTagBits.ALREADY_PROCESSED) == 0) {
					SourceTypeBinding newSourceTypeBinding = buildTypeMethodOfClassScope.invoke(
							classScope, enclosingTypeBinding, packageBinding, (AccessRestriction) null);

					replaceTypeDeclarationBinding(compilationUnit, "", currentTypeDeclaration, newSourceTypeBinding);
				}
			}
		}
	}

	public static void deregisterTypeDeclarations(
			CompilationUnitDeclaration compilationUnit, boolean clearBindingAndScope) {
		TypeDeclaration[] typesInCompilationUnit = compilationUnit.types;
		
		if (typesInCompilationUnit != null) {
			for (TypeDeclaration typeDeclaration : typesInCompilationUnit) {
				deregisterTypeDeclaration(compilationUnit, "", typeDeclaration, clearBindingAndScope);
			}
		}
	}

	private static void deregisterTypeDeclaration(CompilationUnitDeclaration compilationUnit, String namePrefix,
			TypeDeclaration typeDeclaration, boolean clearBindingAndScope) {
//		System.out.println("$$$ Deregistering type declaration: " + new String(typeDeclaration.name));
		
		char[] name = namePrefix.isEmpty() ? typeDeclaration.name :
				new StringBuilder(namePrefix.length() + typeDeclaration.name.length)
					.append(namePrefix).append(typeDeclaration.name).toString().toCharArray();

		setKnownTypeAndKnownPackage(compilationUnit, name, null, null, null, true);

		if (clearBindingAndScope) {
			typeDeclaration.binding = null;
			typeDeclaration.scope = null;
		}

		if (typeDeclaration.memberTypes == null) {
			return;
		}
		
		for (TypeDeclaration memberType : typeDeclaration.memberTypes) {
			String newNamePrefix = namePrefix.isEmpty()
				? new StringBuilder(typeDeclaration.name.length + 1).append(typeDeclaration.name).append('$').toString()
				: new StringBuilder(name.length + 1).append(name).append('$').toString();

			deregisterTypeDeclaration(compilationUnit, newNamePrefix, memberType, clearBindingAndScope);
		}
	}

	private static void replaceTypeDeclarationBinding(CompilationUnitDeclaration compilationUnit,
			String namePrefix, TypeDeclaration currentTypeDeclaration, SourceTypeBinding sourceTypeBinding) {
		char[] name = namePrefix.isEmpty()
				? currentTypeDeclaration.name
				: new StringBuilder(namePrefix.length() + currentTypeDeclaration.name.length)
						.append(namePrefix).append(currentTypeDeclaration.name).toString().toCharArray();

		char[] expectedName = sourceTypeBinding.compoundName[sourceTypeBinding.compoundName.length - 1];

		if (CharOperation.equals(expectedName, name)) {
			setKnownTypeAndKnownPackage(
					compilationUnit, name, name, sourceTypeBinding, sourceTypeBinding.fPackage, false);
		}

		if (currentTypeDeclaration.memberTypes == null) {
			return;
		}

		for (TypeDeclaration memberType : currentTypeDeclaration.memberTypes) {
			String newNamePrefix = namePrefix.isEmpty()
					? new StringBuilder(currentTypeDeclaration.name.length + 1)
							.append(currentTypeDeclaration.name).append('$').toString()
					: new StringBuilder(namePrefix.length() + name.length + 1)
							.append(namePrefix).append(name).append('$').toString();

			replaceTypeDeclarationBinding(compilationUnit, newNamePrefix, memberType, sourceTypeBinding);
		}
	}

	private static void setKnownTypeAndKnownPackage(CompilationUnitDeclaration cud, char[] typeName,
			char[] newKeyTableValue, ReferenceBinding newKnownTypesValue, PackageBinding newKnownPackagesValue,
			boolean rehash) {
		HashtableOfType knownTypes = knownTypesOfPackageBinding.get(cud.scope.fPackage);
		for (int i = 0; i < knownTypes.keyTable.length; i++) {
			if (knownTypes.keyTable[i] == null) {
				continue;
			}

			if (CharOperation.equals(knownTypes.keyTable[i], typeName)) {
				knownTypes.keyTable[i] = newKeyTableValue;
				knownTypes.valueTable[i] = newKnownTypesValue;
			}
		}

		if (rehash) {
			rehashMethodOfHashtableOfType.invoke(knownTypes);
		}

		HashtableOfPackage knownPackages = knownPackagesOfPackageBinding.get(cud.scope.fPackage);
		for (int i = 0; i < knownPackages.keyTable.length; i++) {
			if (knownPackages.keyTable[i] == null) {
				continue;
			}

			if (CharOperation.equals(knownPackages.keyTable[i], typeName)) {
				knownPackages.keyTable[i] = newKeyTableValue;
				knownPackages.valueTable[i] = newKnownPackagesValue;
			}	
		}

		if (rehash) {
			rehashMethodOfHashtableOfPackage.invoke(knownPackages);
		}
	}

	public static long encodePos(int sourceStart, int sourceEnd) {
		return ((long) sourceStart << 32) | sourceEnd;
	}	

	public static boolean isValidTypeReference(TypeBinding type) {
		return type.isValidBinding() && ((type instanceof BinaryTypeBinding) || (type instanceof SourceTypeBinding)
				|| (type instanceof ParameterizedTypeBinding));
	}	

	public static char[][] compoundNameOfClassName(String className) {
		String[] compoundNameParts = className.split("\\.");

		char[][] result = new char[compoundNameParts.length][];
		for (int i = 0; i < compoundNameParts.length; i++) {
			result[i] = compoundNameParts[i].toCharArray();
		}

		return result;
	}

	public static Annotation findAnnotation(
			Annotation[] annotations, Class<? extends java.lang.annotation.Annotation> annotationType) {
		if (annotations == null) {
			return null;
		}

		for (Annotation annotation : annotations) {
			TypeBinding resolvedAnnotationType = annotation.type.resolvedType;	
			
			if ((resolvedAnnotationType == null)
					|| !EclipseProcessorUtils.isValidTypeReference(resolvedAnnotationType)) {
				continue;
			}

			if (CharOperation.equals(compoundNameOfClassName(annotationType.getName()),
					((ReferenceBinding) resolvedAnnotationType).compoundName)) {
				return annotation;
			}
		}

		return null;
	}

	public static boolean compoundNameEquals(char[][] providedCompoundName, Class<?> clazz) {
		char[][] classCompoundName = CharOperation.splitOn('.', clazz.getName().toCharArray());

		return CharOperation.equals(providedCompoundName, classCompoundName);
    }

	public static Annotation findQualifiedAnnotation(Annotation[] annotations, Class<?> annotationType) {
		if (annotations == null) {
			return null;
		}

		char[][] expectedCompoundName = CharOperation.splitOn('.', annotationType.getName().toCharArray());
		
		for (Annotation annotation : annotations) {
			if (!(annotation.type instanceof QualifiedTypeReference)) {
				continue;
			}

			char[][] currentCompoundName = ((QualifiedTypeReference) annotation.type).tokens;
			if (CharOperation.equals(expectedCompoundName, currentCompoundName)) {
				return annotation;
			}
		}

		return null;
	}
	
	public static Class<?> findClass(char[][] compoundName) {
		StringBuilder classNameBuilder = new StringBuilder();
		for (char[] compoundNamePart : compoundName) {
			if (classNameBuilder.length() != 0) {
				classNameBuilder.append('.');
			}

			classNameBuilder.append(compoundNamePart);
		}

		String className = classNameBuilder.toString();

		try {
			return EclipseProcessorUtils.class.getClassLoader().loadClass(className);
		} catch (ClassNotFoundException e) {
			return null;
		}
	}
	
	public static <T> T[] appendToArray(T[] array, Class<T> arrayItemType, T value) {
		int arrayLength = (array != null) ? array.length : 0;

		@SuppressWarnings("unchecked") T[] result = (T[]) Array.newInstance(arrayItemType, arrayLength + 1);
		if (arrayLength > 0) {
			System.arraycopy(array, 0, result, 0, arrayLength);
		}
		result[result.length - 1] = value;

		return result;
	}
	
	private EclipseProcessorUtils() {
		// utility class
	}
}
